package cn.icanci.loopstack.rec.admin.dal.mongodb.mongo;

import cn.icanci.loopstack.rec.admin.dal.mongodb.daointerface.LockDAO;
import cn.icanci.loopstack.rec.admin.dal.utils.IDHolder;
import cn.icanci.loopstack.rec.admin.dal.mongodb.dateobject.LockDO;
import cn.icanci.loopstack.rec.admin.dal.utils.EnvUtils;

import javax.annotation.Resource;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mongodb.core.FindAndModifyOptions;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;

import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;

/**
 * @author icanci
 * @since 1.0 Created in 2022/11/26 16:54
 */
@Service("lockDAO")
public class MongoLockDAO implements LockDAO {
    private static final Logger logger = LoggerFactory.getLogger(MongoLockDAO.class);

    @Resource
    protected MongoTemplate     mongoTemplate;

    @Override
    public String lock(String key, long expireTime) {
        Query query = Query.query(Criteria.where(LockColumn.key).is(key));
        String token = IDHolder.generateNoBySnowFlake("LOCK");
        Update update = new Update().setOnInsert(LockColumn.key, key).setOnInsert(LockColumn.env, EnvUtils.getEnv())
            .setOnInsert(LockColumn.expireAt, System.currentTimeMillis() + expireTime).setOnInsert(LockColumn.token, token);

        FindAndModifyOptions options = new FindAndModifyOptions().upsert(true).returnNew(true);
        LockDO lock = mongoTemplate.findAndModify(query, update, options, COLLECTION_CLASS, COLLECTION_NAME);
        if (lock == null) {
            return StringUtils.EMPTY;
        }
        boolean locked = StringUtils.equals(token, lock.getToken());

        // 如果已过期
        if (!locked && lock.getExpireAt() < System.currentTimeMillis()) {
            DeleteResult deleted = this.mongoTemplate.remove(
                Query.query(Criteria.where(LockColumn.key).is(key).and(LockColumn.token).is(lock.getToken()).and(LockColumn.expireAt).is(lock.getExpireAt())), COLLECTION_CLASS,
                COLLECTION_NAME);
            if (deleted.getDeletedCount() >= 1) {
                // 成功释放锁， 再次尝试获取锁
                return lock(key, expireTime);
            }
        }

        return locked ? token : StringUtils.EMPTY;
    }

    @Override
    public boolean release(String key, String token) {
        Query query = Query.query(Criteria.where(LockColumn.key).is(key).and(LockColumn.token).is(token).and(LockColumn.env).is(EnvUtils.getEnv()));
        DeleteResult deleted = mongoTemplate.remove(query, COLLECTION_CLASS, COLLECTION_NAME);
        boolean released = deleted.getDeletedCount() == 1;
        if (released) {
            logger.info("Remove query successfully affected 1 record for key {} with token {}", key, token);
        } else if (deleted.getDeletedCount() > 0) {
            logger.error("Unexpected result from release for key {} with token {}, released {}", key, token, deleted);
        } else {
            logger.warn("Remove query did not affect any records for key {} with token {}", key, token);
        }
        return released;
    }

    @Override
    public boolean refresh(String key, String token, long expiration) {
        Query query = Query.query(Criteria.where(LockColumn.key).is(key).and(LockColumn.token).is(token).and(LockColumn.env).is(EnvUtils.getEnv()));
        Update update = Update.update(LockColumn.expireAt, System.currentTimeMillis() + expiration);
        UpdateResult updated = mongoTemplate.updateFirst(query, update, COLLECTION_CLASS, COLLECTION_NAME);

        final boolean refreshed = updated.getModifiedCount() == 1;
        if (refreshed) {
            logger.info("Refresh query successfully affected 1 record for key {} " + "with token {}", key, token);
        } else if (updated.getModifiedCount() > 0) {
            logger.error("Unexpected result from refresh for key {} with token {}, " + "released {}", key, token, updated);
        } else {
            logger.warn("Refresh query did not affect any records for key {} with token {}. " + "This is possible when refresh interval fires for the final time "
                        + "after the lock has been released",
                key, token);
        }
        return refreshed;
    }

}
